/*
 ********************************************************************************
 *
 * Copyright (c) 2019-2024 Renesas Electronics Corporation and/or its affiliates
 *
 ********************************************************************************
 */

package com.dialog.suotalib.suota;

import android.content.Context;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.Arrays;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;

public abstract class HeaderInfo {
    private static final String TAG = "HeaderInfo";

    private static final int SIGNATURE_LENGTH = 2;

    private byte[] header;
    private long totalBytes;
    protected ByteBuffer buffer;
    private long payloadSize;
    private long payloadCrc;
    private String version;
    private byte[] versionRaw;
    private long timestamp;

    public abstract int getSignature();
    public abstract String getType();
    public abstract int getHeaderSize();
    public abstract long getPayloadOffset();
    protected abstract int getOffsetPayloadSize();
    protected abstract int getOffsetPayloadCrc();
    protected abstract int getOffsetVersion();
    protected abstract int getVersionLength();
    protected abstract int getOffsetTimestamp();
    protected abstract void initializeTypeSpecific();

    public HeaderInfo(byte[] header, long totalBytes) {
        this.header = header;
        this.totalBytes = totalBytes;
        initialize();
        initializeTypeSpecific();
    }

    public HeaderInfo(@NonNull byte[] raw) {
        header = Arrays.copyOf(raw, getHeaderSize());
        totalBytes = raw.length;
        initialize();
        initializeTypeSpecific();
    }

    private void initialize() {
        buffer = ByteBuffer.wrap(header).order(ByteOrder.LITTLE_ENDIAN);

        payloadSize = buffer.getInt(getOffsetPayloadSize()) & 0xffffffffL;
        payloadCrc = buffer.getInt(getOffsetPayloadCrc()) & 0xffffffffL;
        timestamp = buffer.getInt(getOffsetTimestamp()) & 0xffffffffL;

        buffer.position(getOffsetVersion());
        versionRaw = new byte[getVersionLength()];
        buffer.get(versionRaw);
        int valid = 0;
        for (byte character : versionRaw) {
            if (character != 0 && character != (byte) 0xff)
                valid++;
            else
                break;
        }
        version = new String(versionRaw, 0, valid, Charset.forName("US-ASCII"));

        buffer.rewind();
    }

    @Nullable
    @SuppressWarnings("unchecked")
    static <T extends HeaderInfo> T create(byte[] raw) {
        int signature = ((raw[0] & 0xff) << 8) | (raw[1] & 0xff);

        switch (signature) {
            case HeaderInfo58x.SIGNATURE:
                return (T) new HeaderInfo58x(raw);
            case HeaderInfo68x.SIGNATURE:
                return (T) new HeaderInfo68x(raw);
            case HeaderInfo69x.SIGNATURE:
                return (T) new HeaderInfo69x(raw);
            default:
                return null;
        }
    }

    @Nullable
    public static <T extends HeaderInfo> T create(File file) {
        try {
            return create(file.getAbsolutePath(), new FileInputStream(file), file.length());
        } catch (IOException | SecurityException e) {
            Log.e(TAG, "Failed to read firmware header: " + file.getAbsolutePath(), e);
        }
        return null;
    }

    @Nullable
    public static <T extends HeaderInfo> T create(Context context, DocumentFile file) {
        try {
            return create(file.getUri().toString(), context.getContentResolver().openInputStream(file.getUri()), file.length());
        } catch (IOException e) {
            Log.e(TAG, "Failed to read firmware header: " + file.getUri(), e);
        }
        return null;
    }

    @Nullable
    @SuppressWarnings({"unchecked", "ResultOfMethodCallIgnored"})
    private static <T extends HeaderInfo> T create(String path, InputStream inputStream, long totalBytes) {
        try {
            if (totalBytes < SIGNATURE_LENGTH)
                return null;
            byte[] header = new byte[SIGNATURE_LENGTH];
            inputStream.read(header, 0, SIGNATURE_LENGTH);
            int signature = ((header[0] & 0xff) << 8) | (header[1] & 0xff);

            int headerSize;
            switch (signature) {
                case HeaderInfo58x.SIGNATURE:
                    headerSize = HeaderInfo58x.HEADER_SIZE;
                    break;
                case HeaderInfo68x.SIGNATURE:
                    headerSize = HeaderInfo68x.HEADER_SIZE;
                    break;
                case HeaderInfo69x.SIGNATURE:
                    headerSize = HeaderInfo69x.HEADER_SIZE;
                    break;
                default:
                    return null;
            }

            if (totalBytes < headerSize)
                return null;
            header = Arrays.copyOf(header, headerSize);
            inputStream.read(header, SIGNATURE_LENGTH, headerSize - SIGNATURE_LENGTH);

            switch (signature) {
                case HeaderInfo58x.SIGNATURE:
                    return (T) new HeaderInfo58x(header, totalBytes);
                case HeaderInfo68x.SIGNATURE:
                    return (T) new HeaderInfo68x(header, totalBytes);
                case HeaderInfo69x.SIGNATURE:
                    return (T) new HeaderInfo69x(header, totalBytes);
            }
        } catch (IOException e) {
            Log.e(TAG, "Failed to read firmware header: " + path, e);
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    public byte[] getHeader() {
        return header;
    }

    public long getTotalBytes() {
        return totalBytes;
    }

    public long getPayloadSize() {
        return payloadSize;
    }

    public long getPayloadCrc() {
        return payloadCrc;
    }

    public String getVersion() {
        return version;
    }

    public byte[] getVersionRaw() {
        return versionRaw;
    }

    public long getTimestamp() {
        return timestamp;
    }
}
